package com.hq.common.utils.cipher;

import org.apache.commons.lang.ArrayUtils;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import java.security.*;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPublicKeySpec;

/**

 * @since 2015-02-06 14:15
 */
public final class RSA {
    private static final String ALGORITHM = "RSA";
    private static final String TRANSFORMATION = "RSA/ECB/PKCS1Padding";
    private static final int KEY_SIZE = 512;
    private static final String SIGNATURE_ALGORITHM = "MD5withRSA";
    private static final String SPLIT = "～！@#￥%×";// 不能用半角的字符，不然^$+*在正则表达式中有特殊含义

    private String initKey;
    private RSAPublicKey publicKey;
    private RSAPrivateKey privateKey;

    private RSA() {
    }

    private static class InstanceHolder {
        private static final RSA instance = new RSA();
    }

    public static RSA getInstance() {
        return InstanceHolder.instance;
    }

    public RSA initialize(String key) {
        if (initKey != null) {
            throw new IllegalStateException("已被初始化！");
        }
        if (key == null || key.length() == 0) {
            throw new IllegalArgumentException("密钥串为空！");
        }
        this.initKey = key;
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.decode(key));
        try {
            KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
            PrivateKey privKey = keyFactory.generatePrivate(spec);
            this.privateKey = (RSAPrivateKey) privKey;
            RSAPrivateCrtKey privateCrtKey = (RSAPrivateCrtKey) privKey;
            RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(privateCrtKey.getModulus(), privateCrtKey.getPublicExponent());
            this.publicKey = (RSAPublicKey) keyFactory.generatePublic(publicKeySpec);
            return this;
        } catch (Exception e) {
            throw new RuntimeException("初始化密钥失败！", e);
        }
    }

    public RSAPublicKey getPublicKey() {
        if (this.publicKey == null) {
            throw new IllegalStateException("还未初始化！");
        }
        return this.publicKey;
    }

    /**
     * 从密钥串中提取公钥
     */
    public static RSAPublicKey parsePublicKey(String key) {
        if (key == null || key.length() == 0) {
            throw new IllegalArgumentException("密钥串为空！");
        }
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.decode(key));
        try {
            KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
            PrivateKey privKey = keyFactory.generatePrivate(spec);
            RSAPrivateCrtKey privateCrtKey = (RSAPrivateCrtKey) privKey;
            RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(privateCrtKey.getModulus(), privateCrtKey.getPublicExponent());
            return (RSAPublicKey) keyFactory.generatePublic(publicKeySpec);
        } catch (Exception e) {
            throw new RuntimeException("初始化密钥失败！", e);
        }
    }

    /**
     * 重新生成用于持久化的密钥对
     */
    public static String newKey() {
        KeyPair keyPair = generateKeyPair();
        return Base64.encode(keyPair.getPrivate().getEncoded());
    }

    private static KeyPair generateKeyPair() {
        KeyPairGenerator generator;
        try {
            generator = KeyPairGenerator.getInstance(ALGORITHM);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
        SecureRandom random = new SecureRandom();
        generator.initialize(KEY_SIZE, random);
        return generator.generateKeyPair();
    }

    private Cipher getEncryptCipher() throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException {
        if (this.privateKey == null) {
            throw new IllegalStateException("私钥未能正确初始化！");
        }
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.ENCRYPT_MODE, this.privateKey);
        return cipher;
    }

    private String encodeBASE64(byte[] bytes) {
        return Base64.encode(bytes);
    }

    public String encryptWithSign(String plaintext) throws Exception {
        byte[] rawData = plaintext.getBytes("UTF-8");
        Cipher cipher = getEncryptCipher();
        byte[] result = cipher.doFinal(rawData);
        String encryptStr = encodeBASE64(result);
        String strSign = sign(rawData);
        return encryptStr + SPLIT + strSign;
    }

    public String encrypt(String plaintext) throws Exception {
        byte[] rawData = plaintext.getBytes("UTF-8");
        Cipher cipher = getEncryptCipher();
        byte[] result = null;
        for (int i = 0; i < rawData.length; i += 50) {
            byte[] tmp = cipher.doFinal(ArrayUtils.subarray(rawData, i, i + 50));
            result = ArrayUtils.addAll(result, tmp);
        }
        return encodeBASE64(result);
    }

    private String sign(byte[] rawData) throws GeneralSecurityException {
        Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
        signature.initSign(this.privateKey);
        signature.update(rawData);
        return encodeBASE64(signature.sign());
    }

    private Cipher getDecryptCipher() throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException {
        if (this.publicKey == null) {
            throw new IllegalStateException("公钥未能正确初始化！");
        }
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.DECRYPT_MODE, this.publicKey);
        return cipher;
    }

    public String decrypt(String ciphertext) throws Exception {
        Cipher cipher = getDecryptCipher();
        byte[] rawData = decodeBASE64(ciphertext);
        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i < rawData.length; i += 64) {
            byte[] plainByte = cipher.doFinal(ArrayUtils.subarray(rawData, i, i + 64));
            buffer.append(new String(plainByte, "UTF-8"));
        }
        return buffer.toString();
    }

    public String decryptWithVerify(String ciphertext) throws Exception {
        String[] data = ciphertext.split(SPLIT);
        if (data.length == 2) {
            Cipher cipher = getDecryptCipher();
            byte[] rawData = decodeBASE64(data[0]);
            byte[] plainByte = cipher.doFinal(rawData);
            String plaintext = new String(plainByte, "UTF-8");
            boolean verify = verify(plainByte, data[1]);
            if (verify) {
                return plaintext;
            } else {
                throw new RuntimeException("签名校验失败[" + ciphertext + "]");
            }
        } else {
            throw new IllegalArgumentException("没有同时找到加密串和签名信息[" + ciphertext + "]");
        }
    }

    private boolean verify(byte[] rawData, String sign) throws Exception {
        Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
        signature.initVerify(this.publicKey);
        signature.update(rawData);
        return signature.verify(decodeBASE64(sign));
    }

    private byte[] decodeBASE64(String text) {
        return Base64.decode(text);
    }

    public static void main(String[] args) throws Exception {
        String key = RSA.newKey();
        System.out.println(key);
        RSA rsa = RSA.getInstance().initialize(key);
        String str = rsa.encrypt("geostar|123");
        System.out.println(str);
        System.out.println(rsa.decrypt(str));
    }
}
